[UPDATE] MediaPackage v2がHarvest Jobsをサポートしました![Live-to-VOD]

[UPDATE] MediaPackage v2がHarvest Jobsをサポートしました![Live-to-VOD]

MediaPackage v2でもライブストリームからVODアセットを作成するHarvest Jobが利用可能になりました!タイムシフト再生が可能な状態であること、またEndpointとS3 BucketのPolicyに注意しながら活用しましょう。
Clock Icon2024.10.31

はじめに

清水です。AWS Elemental MediaPackage v2でHarvest Jobがサポートされました!2024/10/31時点ではまだAWS What's Newへのポストはありませんが、AWS Elemental MediaPackage v2 User GuideDocument historyに2024/10/30付けでサポートされた旨が記載されています。

hj01
引用元: Document history for the MediaPackage User Guide - AWS Elemental MediaPackage

ライブストリームからVODアセットを作成するHarvest Job、MediaPackage v1でサポートされたのは2019年10月のことでしたね。(もう5年前です!)

2021年5月にはDASHエンドポイントからのVOD作成もサポートしていました。

このLive-to-VOD機能、個人的にその機能もさることならが Harvest Job という名称も好きで、MediaPackage v2で使えるようになるのを今か今かと待ちわびていました。その名にふさわしい時期にサポートしてくれましたね!本エントリでは実際にMediaPackage v2でHarvest Jobをやってみたのでまとめてみたいと思います。

MediaPackge v2でHarvest Jobするためのリソースについて

MediaPackage v2 User guideの以下ページを参考にしながら、実際にHarvest Jobをやってみます。

Harvest Jobの実行にはStartover windowを設定し、Harvest Jobの対象となる時間内でいわゆるタイムシフト再生が可能な状態である必要があります。この点はMediaPackage v1のHarvest Jobと変わりありませんね。

AWSマネジメントコンソールからOrigin endpointを作成した場合、デフォルトで900秒(15分)のStartover windowが設定されていますが、今回は以下ブログエントリでタイムシフト再生をした環境を利用することにしました。MediaLiveとMediaPackageリソースで構成されています。

MediaPackageまわりの設定を振り返っておくと、ChannelのInput typeはCMAF (CMAF ingestを利用してMediaLiveと連携)、Origin endpointのContainer typeはCMAF、Startover windowは3600(60分)というぐあいです。またEndpoint policyはAttach a public policyを選択しています。(こちらについては、のちほど詳細を確認します。)

ライブストリームの実施についても、上記ブログエントリと同様の環境で検証を行いました。

MediaPackage v2でHarvest Jobをするためのアクセス権限の確認

続いて、MediaPackage v2でHarvest Jobを実施するために必要なアクセス権限まわりを確認します。こちらもUser guideの該当ページを参考に進めます。

主に2つにアクセス権限まわりを確認します。1つ目はMediaPackage v2のendpointに対して与えるHarvestObject権限です。そして2つ目は、Harvest Jobの出力先となるS3バケットのアクセス権限です。こちらはMediaPackage v2のservice pricinpalにPutObjectの権限を付与する必要があります。それぞれ実際にみていきましょう。

Origin endpointに対するHarvestObject権限の付与

MediaPackage v2ではEgressを保護するEndpoint policyがあります。

動画再生時のManifestやSegmentのURLを保護するかたちですね。Harvest JobではOrigin endpointからコンテンツを収集します。そのため、このEgress保護のEndpoint policyでHarvestJobの処理に対する許可を与える必要があるわけです。

この設定例についてはUser Guide内、以下ページ記載があります。

冒頭の MediaPackage L2V Harvester の項目ですね。抜粋すると以下のようになります。

{
    "Version": "2012-10-17",
    "Id": "MediaPackageHarvesterAccessPolicy",
    "Statement": [
        {
            {
                "Sid": "AllowMediaPackageHarvestObjectAccess",
                "Effect": "Allow",
                "Principal": {
                    "Service": "mediapackagev2.amazonaws.com"
                },
                "Condition": {
                    "StringEquals": {
                        "AWS:SourceAccount": "AccountID"
                    }
                },
                "Action": [
                    "mediapackagev2:HarvestObject",
                    "mediapackagev2:GetObject"
                ],
                "Resource": "arn:aws:mediapackagev2:Region:AccountID:channelGroup/ChannelGroupName/channel/ChannelName/originEndpoint/OriginEndpointName
            } 
        }
    ]
}

このポリシーをOrigin endpointに設定するわけですが、先のエントリで作成した(2024/10/31の時点で作成した)Origin endpointのPolicyを確認してみると、以下のように設定されていました。(なお、Attach a public policyで作成したものです。)

{
  "Version" : "2012-10-17",
  "Statement" : [ {
    "Sid" : "AllowPublicGetObjectAccess",
    "Effect" : "Allow",
    "Principal" : "*",
    "Action" : [ "mediapackagev2:GetHeadObject", "mediapackagev2:GetObject" ],
    "Resource" : "arn:aws:mediapackagev2:ap-northeast-1:123456789012:channelGroup/mediapackage-v2-channel-group/channel/time-shifted-viewing-channel/originEndpoint/time-shifted-viewing-origin-endpoint"
  }, {
    "Sid" : "AllowMediaPackageHarvestObjectAccess",
    "Effect" : "Allow",
    "Principal" : {
      "Service" : "mediapackagev2.amazonaws.com"
    },
    "Action" : "mediapackagev2:HarvestObject",
    "Resource" : "arn:aws:mediapackagev2:ap-northeast-1:123456789012:channelGroup/mediapackage-v2-channel-group/channel/time-shifted-viewing-channel/originEndpoint/time-shifted-viewing-origin-endpoint",
    "Condition" : {
      "StringEquals" : {
        "AWS:SourceAccount" : "123456789012"
      }
    }
  } ]
}

hj02

改めてCreate origin endpointページのEndpoint policyの項目、Attach a public policyに設定した場合のPolicyを確認してみます。下のほうにAllowMediaPackageHarvestObjectAccessとして追加されていますね。

hj03

ただし、以前Attach a public policyで作成したOrigin endpointに自動で権限が追加されている、ということは ありませんでした。 あらたにEndpointを作成する場合は、Attach a public policyの場合には権限を追加する必要はありませんが、これまでに作成済みのEndpointを使用する場合は権限を追加する必要がある点に注意しましょう。

Harvest Jobの出力先S3バケットに対するPutObject権限の付与

上記のEndpoint policyに加えて、Harvest Jobの出力先となるS3バケットに対してPutObjectの権限も必要です。User GuideにはMediaPackage v2 service principal権限を与えるよう記載されていますね。

実はこちらの権限、具体的な内容がUser Guideに記載されていなかったことから、設定せずにHarvest Jobを実施(作成)しようとしてみたのですが、以下のエラーが発生してしまいました。

Harvest job create operation failed
ValidationException: Validation error encountered at request.s3DestinationConfig: Unable to write to S3 destination bucket. Verify MediaPackageV2 service principal mediapackagev2.amazonaws.com is allow listed write access to the bucket and the bucket region is in the same region.

hj04

このメッセージ、つまりMediaPackageV2 service principalとしてmediapackagev2.amazonaws.comに権限を与えよ、ということをヒントに出力先S3バケットのバケットポリシーを設定します。ELB(ALB)のアクセスログ設定を参考にしてみました。

「Step 2: Attach a policy to your S3 bucket」の「Regions available as of August 2022 or later」に対するポリシーを参考にします。以下の内容ですね。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "logdelivery.elasticloadbalancing.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "s3-bucket-arn"
    }
  ]
}

ここで、"Service": "logdelivery.elasticloadbalancing.amazonaws.com"の箇所を"Service": "mediapackagev2.amazonaws.com"に置き換えます。また"Resource": "s3-bucket-arn""Resource": "arn:aws:s3:::content-source-xxxxxxxx/*"というように、実際のS3バケット名と許可対象のパスに置き換えました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid" : "AllowMediaPackageHarvestJobPutObject",
      "Effect": "Allow",
      "Principal": {
        "Service": "mediapackagev2.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::content-source-xxxxxxxx/*"
    }
  ]
}

ただし、これだけだとAWSアカウント関係なく、Harvest Jobを行うMediaPackage v2リソースに許可が与えられてしまうのでは?と考え(実際にそうなってしまうかは未確認ではありますが)、先ほどのEndpoint policyを参考に以下のように"AWS:SourceAccount"の条件を加えます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid" : "AllowMediaPackageHarvestJobPutObject",
      "Effect": "Allow",
      "Principal": {
        "Service": "mediapackagev2.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::content-source-xxxxxxxx/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceAccount": "123456789012"
        }
      }
    }
  ]
}

今回Harvest Jobの出力先に使用するS3バケットcontent-source-xxxxxxxxですが、実際には別の検証で利用したものを流用したことなどから、PublicReadの権限が付与されています。そのため、最終的には以下のBucket policyを設定するかたちとなりました。

{
    "Version": "2012-10-17",
    "Id": "PublicReadForGetBucketObjects",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::content-source-xxxxxxxx/*"
        },
        {
            "Sid": "AllowMediaPackageHarvestJobPutObject",
            "Effect": "Allow",
            "Principal": {
                "Service": "mediapackagev2.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::content-source-xxxxxxxx/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceAccount": "123456789012"
                }
            }
        }
    ]
}

hj05

MediaPackage v2でHarvest Jobをやってみた

それでは実際にMediaPackage v2でHarvest Jobを作成してみます。対象のMediaPackage v2のChannelリソース詳細画面、Harvest Jobsの項目に進みます。(Newマークがついていますね!)[Create harvest job]ボタンを押下します。

hj06

Harvest job nameとDescriptionを適切に設定します。Cannel group nameとChannel nameは変更ができないようでした。選択されているものをそのまま利用します。Origin endpoint nameで対象とするendpointを選択します。またHarvest manifests nameでこちらも対象とするmanifest definitionsを選択します。

hj07

Schedule configurationでStart date and timeならびにEnd date and timeを入力します。Destinationでは、出力先となるS3バケットを選択、pathについても入力しておきます。

hj08

[Create harvest job]ボタンでHarvest Jobを作成しましょう。In progressのStatusでJobの作成に成功しました。

hj09

Harvest Jobの詳細画面は以下のようなぐあいです。

hj10

hj11

今回は体感ですが2,3分もかからないうちにJobがCompletedとなりました。

hj12

出力先となるS3バケットを確認してみます。直下にdestination pathとして指定したemp-v2-harvest-jobという(便宜上の)フォルダが作成され、その中に収穫物が格納されていますね。

hj13

hj14

先ほど述べたとおり、このS3バケットは別検証で利用したものを流用しており、Public Access可能にしているほかCORSについても設定してみます。hls.js demoのページでManifestファイルのURLを入力、再生を確認してみました。

hj15

hj16

Harvest jobで作成したVODコンテンツが再生できました!

hj17

Harvest jobで作成されたManifestファイルを確認

無事にMediaPackage v2のHarvest JobでVODアセットが作成できました。このVODアセットのManifestファイルについても確認してみます。S3内のManifestファイルのURLにcurlコマンドでアクセスしました。

% curl https://content-source-xxxxxxxx.s3.ap-northeast-1.amazonaws.com/emp-v2-harvest-job/index.m3u8
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:LANGUAGE="und",AUTOSELECT=YES,CHANNELS="2",TYPE=AUDIO,URI="variant_audio_aac.m3u8",GROUP-ID="audio_0",DEFAULT=NO,NAME="und"
#EXT-X-STREAM-INF:CODECS="mp4a.40.2,avc1.640028",AVERAGE-BANDWIDTH=5000000,RESOLUTION=1920x1080,VIDEO-RANGE=SDR,FRAME-RATE=29.97,BANDWIDTH=5000000,AUDIO="audio_0"
variant_video_1080p30.m3u8
#EXT-X-STREAM-INF:CODECS="mp4a.40.2,avc1.64001F",AVERAGE-BANDWIDTH=3000000,RESOLUTION=1280x720,VIDEO-RANGE=SDR,FRAME-RATE=29.97,BANDWIDTH=3000000,AUDIO="audio_0"
variant_video_720p30.m3u8
% curl https://content-source-xxxxxxxx.s3.ap-northeast-1.amazonaws.com/emp-v2-harvest-job/variant_video_1080p30.m3u8
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:288106095
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-MAP:URI="segment_video_1080p30_288104887_init.mp4"
#EXTINF:6.006,
segment_video_1080p30_288106095.mp4
#EXTINF:6.006,
segment_video_1080p30_288106096.mp4
#EXTINF:6.006,
segment_video_1080p30_288106097.mp4
#EXTINF:6.006,
segment_video_1080p30_288106098.mp4
#EXTINF:6.006,
segment_video_1080p30_288106099.mp4
#EXTINF:6.006,
segment_video_1080p30_288106100.mp4
#EXTINF:6.006,
segment_video_1080p30_288106101.mp4
#EXTINF:6.006,
segment_video_1080p30_288106102.mp4
#EXTINF:6.006,
segment_video_1080p30_288106103.mp4
#EXTINF:6.006,
segment_video_1080p30_288106104.mp4
#EXTINF:6.006,
segment_video_1080p30_288106105.mp4
#EXTINF:6.006,
segment_video_1080p30_288106106.mp4
#EXTINF:6.006,
segment_video_1080p30_288106107.mp4
#EXTINF:6.006,
segment_video_1080p30_288106108.mp4
#EXTINF:6.006,
segment_video_1080p30_288106109.mp4
#EXTINF:6.006,
segment_video_1080p30_288106110.mp4
#EXTINF:6.006,
segment_video_1080p30_288106111.mp4
#EXTINF:6.006,
segment_video_1080p30_288106112.mp4
#EXTINF:6.006,
segment_video_1080p30_288106113.mp4
#EXTINF:6.006,
segment_video_1080p30_288106114.mp4
#EXTINF:6.006,
segment_video_1080p30_288106115.mp4
#EXTINF:6.006,
segment_video_1080p30_288106116.mp4
#EXTINF:6.006,
segment_video_1080p30_288106117.mp4
#EXTINF:6.006,
segment_video_1080p30_288106118.mp4
#EXTINF:6.006,
segment_video_1080p30_288106119.mp4
#EXTINF:6.006,
segment_video_1080p30_288106120.mp4
#EXTINF:6.006,
segment_video_1080p30_288106121.mp4
#EXTINF:6.006,
segment_video_1080p30_288106122.mp4
#EXTINF:6.006,
segment_video_1080p30_288106123.mp4
#EXTINF:6.006,
segment_video_1080p30_288106124.mp4
#EXTINF:6.006,
segment_video_1080p30_288106125.mp4
#EXTINF:6.006,
segment_video_1080p30_288106126.mp4
#EXTINF:6.006,
segment_video_1080p30_288106127.mp4
#EXTINF:6.006,
segment_video_1080p30_288106128.mp4
#EXTINF:6.006,
segment_video_1080p30_288106129.mp4
#EXTINF:6.006,
segment_video_1080p30_288106130.mp4
#EXTINF:6.006,
segment_video_1080p30_288106131.mp4
#EXTINF:6.006,
segment_video_1080p30_288106132.mp4
#EXTINF:6.006,
segment_video_1080p30_288106133.mp4
#EXTINF:6.006,
segment_video_1080p30_288106134.mp4
#EXTINF:6.006,
segment_video_1080p30_288106135.mp4
#EXTINF:6.006,
segment_video_1080p30_288106136.mp4
#EXTINF:6.006,
segment_video_1080p30_288106137.mp4
#EXTINF:6.006,
segment_video_1080p30_288106138.mp4
#EXTINF:6.006,
segment_video_1080p30_288106139.mp4
#EXTINF:6.006,
segment_video_1080p30_288106140.mp4
#EXTINF:6.006,
segment_video_1080p30_288106141.mp4
#EXTINF:6.006,
segment_video_1080p30_288106142.mp4
#EXTINF:6.006,
segment_video_1080p30_288106143.mp4
#EXTINF:6.006,
segment_video_1080p30_288106144.mp4
#EXT-X-ENDLIST

Harvest Jobの指定時間はStart time (local)が2024/10/31 18:00:00 (UTC+09:00)、End time (local)が2024/10/31 18:05:00 (UTC+09:00)でした。同じ時間帯にMediaPackage v2でタイムシフト再生した際のManifestファイルについても確認してみましょう。同様の内容となっていることが確認できますね。(ぱっとみた限りですが、Child manifestは同じ内容、ManifestファイルのURIが少し異なるぐらいでしょうか。)

% curl "https://xxxxxx.egress.xxxxxx.mediapackagev2.ap-northeast-1.amazonaws.com/out/v1/mediapackage-v2-channel-group/time-shifted-viewing-channel/time-shifted-viewing-origin-endpoint/index.m3u8?start=2024-10-31T09:00:00Z&end=2024-10-31T09:05:00Z"
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MEDIA:LANGUAGE="und",AUTOSELECT=YES,CHANNELS="2",TYPE=AUDIO,URI="variant_audio_aac.m3u8?start=2024-10-31T09:00:00Z&end=2024-10-31T09:05:00Z",GROUP-ID="audio_0",DEFAULT=NO,NAME="und"
#EXT-X-STREAM-INF:CODECS="mp4a.40.2,avc1.640028",AVERAGE-BANDWIDTH=5000000,RESOLUTION=1920x1080,VIDEO-RANGE=SDR,FRAME-RATE=29.97,BANDWIDTH=5000000,AUDIO="audio_0"
variant_video_1080p30.m3u8?start=2024-10-31T09:00:00Z&end=2024-10-31T09:05:00Z
#EXT-X-STREAM-INF:CODECS="mp4a.40.2,avc1.64001F",AVERAGE-BANDWIDTH=3000000,RESOLUTION=1280x720,VIDEO-RANGE=SDR,FRAME-RATE=29.97,BANDWIDTH=3000000,AUDIO="audio_0"
variant_video_720p30.m3u8?start=2024-10-31T09:00:00Z&end=2024-10-31T09:05:00Z
% curl "https://xxxxxx.egress.xxxxxx.mediapackagev2.ap-northeast-1.amazonaws.com/out/v1/mediapackage-v2-channel-group/time-shifted-viewing-channel/time-shifted-viewing-origin-endpoint/variant_video_1080p30.m3u8?start=2024-10-31T09:00:00Z&end=2024-10-31T09:05:00Z"
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:288106095
#EXT-X-DISCONTINUITY-SEQUENCE:0
#EXT-X-MAP:URI="segment_video_1080p30_288104887_init.mp4"
#EXTINF:6.006,
segment_video_1080p30_288106095.mp4
#EXTINF:6.006,
segment_video_1080p30_288106096.mp4
#EXTINF:6.006,
segment_video_1080p30_288106097.mp4
#EXTINF:6.006,
segment_video_1080p30_288106098.mp4
#EXTINF:6.006,
segment_video_1080p30_288106099.mp4
#EXTINF:6.006,
segment_video_1080p30_288106100.mp4
#EXTINF:6.006,
segment_video_1080p30_288106101.mp4
#EXTINF:6.006,
segment_video_1080p30_288106102.mp4
#EXTINF:6.006,
segment_video_1080p30_288106103.mp4
#EXTINF:6.006,
segment_video_1080p30_288106104.mp4
#EXTINF:6.006,
segment_video_1080p30_288106105.mp4
#EXTINF:6.006,
segment_video_1080p30_288106106.mp4
#EXTINF:6.006,
segment_video_1080p30_288106107.mp4
#EXTINF:6.006,
segment_video_1080p30_288106108.mp4
#EXTINF:6.006,
segment_video_1080p30_288106109.mp4
#EXTINF:6.006,
segment_video_1080p30_288106110.mp4
#EXTINF:6.006,
segment_video_1080p30_288106111.mp4
#EXTINF:6.006,
segment_video_1080p30_288106112.mp4
#EXTINF:6.006,
segment_video_1080p30_288106113.mp4
#EXTINF:6.006,
segment_video_1080p30_288106114.mp4
#EXTINF:6.006,
segment_video_1080p30_288106115.mp4
#EXTINF:6.006,
segment_video_1080p30_288106116.mp4
#EXTINF:6.006,
segment_video_1080p30_288106117.mp4
#EXTINF:6.006,
segment_video_1080p30_288106118.mp4
#EXTINF:6.006,
segment_video_1080p30_288106119.mp4
#EXTINF:6.006,
segment_video_1080p30_288106120.mp4
#EXTINF:6.006,
segment_video_1080p30_288106121.mp4
#EXTINF:6.006,
segment_video_1080p30_288106122.mp4
#EXTINF:6.006,
segment_video_1080p30_288106123.mp4
#EXTINF:6.006,
segment_video_1080p30_288106124.mp4
#EXTINF:6.006,
segment_video_1080p30_288106125.mp4
#EXTINF:6.006,
segment_video_1080p30_288106126.mp4
#EXTINF:6.006,
segment_video_1080p30_288106127.mp4
#EXTINF:6.006,
segment_video_1080p30_288106128.mp4
#EXTINF:6.006,
segment_video_1080p30_288106129.mp4
#EXTINF:6.006,
segment_video_1080p30_288106130.mp4
#EXTINF:6.006,
segment_video_1080p30_288106131.mp4
#EXTINF:6.006,
segment_video_1080p30_288106132.mp4
#EXTINF:6.006,
segment_video_1080p30_288106133.mp4
#EXTINF:6.006,
segment_video_1080p30_288106134.mp4
#EXTINF:6.006,
segment_video_1080p30_288106135.mp4
#EXTINF:6.006,
segment_video_1080p30_288106136.mp4
#EXTINF:6.006,
segment_video_1080p30_288106137.mp4
#EXTINF:6.006,
segment_video_1080p30_288106138.mp4
#EXTINF:6.006,
segment_video_1080p30_288106139.mp4
#EXTINF:6.006,
segment_video_1080p30_288106140.mp4
#EXTINF:6.006,
segment_video_1080p30_288106141.mp4
#EXTINF:6.006,
segment_video_1080p30_288106142.mp4
#EXTINF:6.006,
segment_video_1080p30_288106143.mp4
#EXTINF:6.006,
segment_video_1080p30_288106144.mp4
#EXT-X-ENDLIST

まとめ

AWS Elemental MediaPackage v2で新たにサポートしたHarvest Jobsについて、実際にライブストリームからVODアセットを作成して動作を確認してみました。

MediaPackageのEndponit側でタイムシフト再生が可能になっている必要があるなど、基本的にはMediaPackage v1と同様の操作感かと思います。ただし、権限まわりの設定については異なります。見返してみるとMediaPackage v1のころはS3バケットへの書き込み権限を設定したIAMロールを作成し、Harvest Job作成の際にそのIAMロールを指定するかたちでした。

MediaPackage v2ではPolicyまわりがよりAWSらしく強化されています。そのため、MediaPackage v2のHarvest Jobでも、Endpoint policyでアクセス許可を設定します。また出力先となるS3バケット側ではBucket policyでアクセス許可を行いました。このアクセス権限、ポリシーまわりがv1と異なることに注意しながら、MediaPackage v2でもHarvest Jobを活用していきましょう!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.